'''
@author：fc
@date：  2021/11/10
@contact：675435108@qq.com
'''
import math
import json
import os
import random

"""
文件内容&功能简要：
# 顾名思义：该文件实现基于用户的协同过滤算法，使用的数据集为ml-1m的评分数据集，给用户推荐和其相似的用户喜欢的item
"""


class UserCFRec:
    def __init__(self,datafile):
        self.dataFile = datafile
        self.data = self.loadData()
        self.trainData,self.testData = self.updateSplitData(3,47) # 训练集和数据集
        self.userSim = self.UserSimilarityBest()

    # 加载评分数据到data中
    def loadData(self):
        print("加载数据...")
        data = []
        for line in open(self.dataFile):  # 这里open（）函数竟然打开后直接就读取行了
            userId,itemID,record,_=line.split("::")
            data.append((userId,itemID,int(record))) # list里面存储的是元组
        return data

    """
       拆分数据集为训练集和测试集，训练集和测试集1:4，每隔四次添加一次到测试集
    """

    def updateSplitData(self,k,seed,M=8):
        print("训练集和测试集切分...")
        train,test={},{}
        i=1
        # self.data = random.shuffle(self.data)
        for user,item,record in self.data:
            if i == 5:
                test.setdefault(user,{})
                test[user][item] = record
                i=1
            else:
                train.setdefault(user,{})
                train[user][item] = record  # 单个值不用再setdefault()了
                i += 1
        return train,test

    # 计算用户之间的相似度，采用惩罚热门商品和优化算法复杂度的算法
    def UserSimilarityBest(self):
        print("开始计算用户之间的相似度...")
        if os.path.exists("./reml-1l/updateSplitData/user_sim.json"):
            print("用户相似度从文件加载")
            userSim = json.load(open("./reml-1l/updateSplitData/user_sim.json","r"))
        else:
            # 得到每个item被哪些用户评价过
            item_users = dict()
            for u,items in self.trainData.items(): # items()好像m是dict()对象的属性
                for i in items.keys():
                    item_users.setdefault(i,set())
                    if self.trainData[u][i]>0:  # 是否有用户对item评分为0，评分为0的项应该要取用吗？=>其实，大于两分的才算是喜欢，还要细分。
                        item_users[i].add(u)  # add()应该是tuple对象的属性，因为append是list对象的属性
            # 构建倒排表
            count = dict()   # 倒排表
            user_item_count = dict()  # 存储每个用户交互过的item总数
            for i,users in item_users.items():  # 其实这里也就两层循环
                for u in users:
                    user_item_count.setdefault(u,0)
                    user_item_count[u] += 1
                    # 计算用户u和用户v交集的item数目，每个item都加了惩罚因子，item中的活跃用户数目越大，惩罚数就越大，
                    count.setdefault(u, {})
                    for v in users:
                        count[u].setdefault(v,0)
                        if u == v:
                            continue
                        # 活跃用户惩罚项，如果不加惩罚因子的话，这里直接加1，遍历所有电影item后，count[u][v]的值就是用户u和用户v交集的电影总数
                        count[u][v] += 1/math.log(1+len(users))

            # 构建相似度矩阵
            userSim = dict()
            for u, related_users in count.items(): # 这样想来count里面有重复项，即count[u][v]和count[v][u]是一样的，按理说，只要三角矩阵便可
                userSim.setdefault(u,{})
                for v,cuv in related_users.items():
                    if u ==v:
                        continue
                    userSim[u].setdefault(v,0.0)
                    userSim[u][v] = cuv / math.sqrt(user_item_count[u] * user_item_count[v])
            json.dump(userSim,open("./reml-1l/updateSplitData/user_sim.json","w"))

        return userSim

    """
       为用户进行物品推荐
           user:为用户user进行物品推荐
           k:选取k个紧邻用户
           nitems:取nitems个物品
    """
    def recommend(self,user,k=8,nitems=40):
        result = dict()  # 存储用户user没有访问过的item,以及和user最相似的k个用户对该用户该item评分乘上相似性的和
        hava_score_items = self.trainData.get(user,{})
        # v和wuv分别是和user最相似的k个用户中一个和其对应的相似性
        # 遍历最相似的k个用户，
        for v,wuv in sorted(self.userSim[user].items(),key=lambda x:x[1],reverse=True)[0:k]:
            # 遍历用户v交互过的所有item，留下user没有交互过的item
            for i,rvi in self.trainData[v].items():
                if i in hava_score_items: # 如果该item被user访问过，直接跳过
                    continue
                result.setdefault(i,0)
                # 计算该相似的用户对该item的评分*相似性，最终得到k个相似用户*相似性*评分的累积和
                result[i] += wuv*rvi
        # 返回评分最高的nitems数目的排序好的items字典
        newDict = dict(sorted(result.items(), key=lambda x: x[1], reverse=True)[0:nitems])
        return newDict

    """
       计算准确率
         k:近邻用户数
         nitems:推荐的item个数
    """
    def precision(self,k=8,nitems=10):
        print("开始计算准确率...")
        hit = 0.5
        precision = 0
        for user in self.trainData.keys():
            tu = self.testData.get(user,{}) # 这里和self.testData.get(user)功能一样
            # 得到用户user未访问过nitems个根据对itema可能性评分的排序好的字典
            rank = self.recommend(user,k=k,nitems=nitems)
            for item,rate in rank.items():
                # 如果该item存在测试集（用户确实评分过）的中,则代表命中
                if item in tu:
                    hit += 1
            # 其实我这里可以用 hit/nitems得到每个user的命中率，当然所有user的命中总数除上总的nitem数也行
            precision += nitems
        return hit/(precision*1.0)

def set_default(obj):
     if isinstance(obj, set):
         return list(obj)
     raise TypeError

if __name__ == '__main__':
    dataPath = "./data/ml-1m/ratings.dat"
    cf = UserCFRec(dataPath)
    result = cf.recommend("1")
    print("user '1' recommend result is {} ".format(result))

    precision = cf.precision()
    print("precision is {}".format(precision))
